package com.bigknow.appstore.web.framework.dao;

import com.bigknow.appstore.web.framework.util.GlobalConstant;
import com.bigknow.appstore.common.util.StringUtils;
import com.bigknow.appstore.web.framework.common.BaseLogger;
import com.bigknow.appstore.web.framework.entity.IEntity;
import com.bigknow.appstore.web.framework.entity.LoadRefField;
import com.bigknow.appstore.web.framework.entity.QueryField;
import com.bigknow.appstore.web.framework.vo.Page;
import org.mongodb.morphia.Datastore;
import org.mongodb.morphia.aggregation.AggregationPipeline;
import org.mongodb.morphia.aggregation.Group;
import org.mongodb.morphia.aggregation.Sort;
import org.mongodb.morphia.annotations.Transient;
import org.mongodb.morphia.query.CriteriaContainerImpl;
import org.mongodb.morphia.query.Query;
import org.springframework.beans.factory.annotation.Autowired;

import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.util.*;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * Created by TwelveFoldEr on 2016/3/8.
 */
public abstract class AbstractDao<T extends IEntity> extends BaseLogger implements IDao<T> {
    protected AtomicLong counter = new AtomicLong(System.currentTimeMillis());

    @Autowired
    protected Datastore datastore;

    protected Class<T> entityClass = (Class<T>) ((ParameterizedType) getClass()
            .getGenericSuperclass()).getActualTypeArguments()[0];
    protected List<Field> queryFields = findQueryFields();//For contains;
    private List<Field> fields = findFields();//For equal;
    private Map<String, Field> fieldMap = findFields().stream().collect(Collectors.toMap(Field::getName, x -> x));
    private List<Field> loadRefFields = findLoadRefFields();//For load ref;

    private List<Field> findFields(Function<Field, Boolean> function) {
        List<Field> fields = new ArrayList<>();
        Field[] entityFields = entityClass.getDeclaredFields();
        for (int i = 0; i < entityFields.length; i++) {
            Field field = entityFields[i];
            field.setAccessible(true);
            if (function.apply(field)) {
                fields.add(field);
            }
        }

        return fields;
    }

    /**
     * No include id field.Because query id use method get.
     *
     * @return
     */
    private List<Field> findQueryFields() {
        List<Field> list = findFields(field -> field.isAnnotationPresent(QueryField.class));
        if (list.isEmpty()) list = findFields(field -> isValidQueryField(field));
        return list;
    }

    private List<Field> findFields() {
        return findFields(field -> isValidQueryField(field));
    }

    private List<Field> findLoadRefFields() {
        return findFields(field -> field.isAnnotationPresent(LoadRefField.class));
    }

    /**
     * 由于int、long、float、double基础类型有默认值，所以所有查询都排除这些类型。
     *
     * @param field
     * @return
     */
    private boolean isValidQueryField(Field field) {
        if (field.isAnnotationPresent(Transient.class)) return false;
        if (field.getName() == "serialVersionUID") return false;
        QueryField[] queryFields = field.getAnnotationsByType(QueryField.class);
        if (queryFields != null && queryFields.length > 0) {
            return queryFields[0].enable();
        } else if (field.getType().getSimpleName().equalsIgnoreCase("String") || field.getType().isEnum() || field.getType().getSimpleName().equalsIgnoreCase("boolean")) {
            return true;
        }

        return false;
    }

    @Override
    public T get(String id) {
        return datastore.get(entityClass, id);
    }

    @Override
    public List<T> findById(String... ids) {
        Set<String> set = new HashSet<>();
        for (int i = 0; i < ids.length; i++) {
            String idStr = ids[i];
            if (StringUtils.isNotBlank(idStr)) {
                if (idStr.contains(GlobalConstant.COMBINATION_SEPARATOR)) {
                    String[] id = idStr.split(GlobalConstant.COMBINATION_SEPARATOR);
                    for (int j = 0; j < id.length; j++) {
                        if (StringUtils.isNotBlank(id[j])) set.add(id[j]);
                    }
                } else {
                    set.add(idStr);
                }
            }
        }

        List<T> list = new ArrayList<>();
        set.forEach(id -> {
            T t = get(id);// TODO change to in.
            if (t != null) {
                list.add(t);
            }
        });

        return list;
    }

    public List<T> findByQuery(T t, Consumer<Query<T>> consumer) {
        Query<T> q = datastore.find(entityClass);
        consumer.accept(q);

        return (List<T>) q.asList();
    }

    @Override
    public List<T> find(T t) {
        Query<T> q = prepareEqualQuery(t);
        return (List<T>) q.asList();
    }

    @Override
    public List<T> find(T t, int limit) {
        Query<T> q = prepareEqualQuery(t);
        return (List<T>) q.limit(limit).asList();
    }

    @Override
    public List<T> findWithFuzzy(T t) {
        Query<T> q = prepareContainsQuery(t);
        return (List<T>) q.asList();
    }

    @Override
    public T findFirst(T t) {
        List<T> list = find(t);
        if (list != null && !list.isEmpty()) {
            return list.get(0);
        }
        return null;
    }

    @Override
    public Page<T> find(T t, int pageIndex, int pageSize) {
        Query<T> query = datastore.createQuery(entityClass);
        if (t != null) {
            for (Field field : fields) {
                try {
                    Object value = field.get(t);
                    if (value != null && StringUtils.isNotBlank(value.toString())) {
                        // All type process as string.
                        if (boolean.class.getSimpleName().equalsIgnoreCase(field.getType().getName())) {
                            query.filter(field.getName(), Boolean.valueOf(value.toString()));
                        } else {
                            query.field(field.getName()).equal(value.toString());
                        }
                    }
                } catch (IllegalAccessException e) {
                    this.logger.warn("Page find error.", e);
                }
            }
            addQueryForFind(t, query);
        }
        return toPage(t.getOrderBy(), query, pageIndex, pageSize);
    }

    protected Query<T> addQueryForFind(T t, Query<T> query) {
        return query;
    }

    @Override
    public Page<T> findWithFuzzy(T t, int pageIndex, int pageSize) {
        Query<T> query = datastore.createQuery(entityClass);
        if (t != null) {
            List<String> fuzzyFieldNames = new ArrayList<>();
            for (Field field : queryFields) {
                try {
                    Object value = field.get(t);
                    if (value != null && StringUtils.isNotBlank(value.toString())) {
                        // All type process as string.
                        query.filter(field.getName(), value);
//                        query.field(field.getName()).contains(value.toString());
                    } else if (StringUtils.isNotBlank(t.getFuzzyField())) {
                        fuzzyFieldNames.add(field.getName());
                    }
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }

            addCriteriaWithFuzzField(t, query, fuzzyFieldNames);
        }

        return toPage(t.getOrderBy(), query, pageIndex, pageSize);
    }

    protected void addCriteriaWithFuzzField(T t, Query<T> query, List<String> fuzzyFieldNames) {
        if (StringUtils.isNotBlank(t.getFuzzyField())) {
            CriteriaContainerImpl[] criteriaContainers = new CriteriaContainerImpl[fuzzyFieldNames.size()];
            for (int i = 0; i < fuzzyFieldNames.size(); i++) {
                criteriaContainers[i] = query.criteria(fuzzyFieldNames.get(i)).contains(t.getFuzzyField());
            }
            query.or(criteriaContainers);
        }
    }

    @Override
    public List<T> findLimit(int limit) {
        return datastore.find(entityClass).limit(limit).asList();
    }

    /**
     * Find all records.
     *
     * @return
     */
    @Override
    public List<T> findAll() {
        return datastore.find(entityClass).asList();
    }

    /**
     * If key exist update,else insert.
     *
     * @param t
     */
    @Override
    public int save(T t) {
        datastore.save(t);
        // Not exception maybe save success.
        return 1;
    }

    @Override
    public int delete(String id) {
        T t = get(id);
        if (t == null) return 0;
        else return datastore.delete(t).getN();
    }

    @Override
    public long getCount() {
        return datastore.getCount(entityClass);
    }

    @Override
    public long getCount(T t) {
        return prepareEqualQuery(t).countAll();
    }

    private Query<T> prepareEqualQuery(T t) {
        Query<T> q = datastore.find(entityClass);
        fields.forEach(field -> {
            try {
                if (field.get(t) != null) {
//                    field.setAccessible(true);
                    q.field(field.getName()).equal(field.get(t));
                }
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        });
        return q;
    }

    private Query<T> prepareContainsQuery(T t) {
        Query<T> q = datastore.find(entityClass);
        queryFields.forEach(field -> {
            try {
                if (field.get(t) != null)
                    q.field(field.getName()).contains(field.get(t).toString());
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        });
        return q;
    }

    public <E> Iterator<E> preparePipeline(Class<E> pClass,
                                           Consumer<Query<T>> queryConsumer,
                                           Consumer<List<Group>> idsConsumer,
                                           Consumer<List<Group>> groupsConsumer,
                                           Consumer<List<Sort>> sortsConsumer, int limit) {
        Query<T> query = this.datastore.createQuery(entityClass);
        queryConsumer.accept(query);

        List<Group> ids = new ArrayList<>();
        idsConsumer.accept(ids);

        List<Group> groups = new ArrayList<>();
        groupsConsumer.accept(groups);

        List<Sort> sorts = new ArrayList<>();
        sortsConsumer.accept(sorts);

        return preparePipeline(pClass, query, ids, groups, sorts, limit);
    }

    public <E> Iterator<E> preparePipeline(Class<E> pClass, Query<T> query, List<Group> ids,
                                           List<Group> groups, List<Sort> sorts, int limit) {
        AggregationPipeline pipeline = this.datastore.createAggregation(entityClass)
                .match(query)
                .group(ids, groups.toArray(new Group[groups.size()]));

        if (!sorts.isEmpty()) pipeline.sort(sorts.toArray(new Sort[sorts.size()]));

        if (limit <= 0)
            return pipeline.aggregate(pClass);
        else
            return pipeline.limit(limit).aggregate(pClass);
    }

    protected Page<T> toPage(String orderBy, Query<T> query, int pageIndex, int pageSize) {
        Page<T> page = new Page<>();
        page.setTotalNum(query.countAll());
        if (!org.apache.commons.lang3.StringUtils.isEmpty(orderBy)) {
            page.setEntities(query.offset((pageIndex - 1) * pageSize).limit(pageSize).order(orderBy).asList());
            page.setOrderBy(orderBy);
        } else {
            page.setEntities(query.offset((pageIndex - 1) * pageSize).limit(pageSize).asList());
        }
        page.setPageIndex(pageIndex);
        page.setPageSize(pageSize);

        long pagesNum = page.getTotalNum() / pageSize;
        if (page.getTotalNum() % pageSize != 0) pagesNum++;
        page.setPagesNum(pagesNum);

        if (page.getTotalNum() == 0) page.setStartRow(0);
        else page.setStartRow(pageIndex * pageSize - (pageSize - 1));
        if ((pageIndex) * pageSize >= page.getTotalNum()) page.setEndRow(page.getTotalNum());
        else page.setEndRow((pageIndex) * pageSize);

        loadRefField(page.getEntities());
        return page;
    }

    private List<T> loadRefField(List<T> list) {
        list.forEach(x -> {
            this.loadRefFields.forEach(y -> {
                try {
                    LoadRefField lrf = y.getAnnotation(LoadRefField.class);
                    if (fieldMap.containsKey(lrf.field())) {
                        String keyValue = (String) fieldMap.get(lrf.field()).get(x);
                        Object ref = this.datastore.find(y.getType()).field(lrf.key()).equal(keyValue).get();
                        if (ref != null) y.set(x, ref);
                    }
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            });
        });

        return list;
    }

}
